---
id: friendly-exception
title: 5. 友好异常处理
sidebar_label: 5. 友好异常处理
---

import useBaseUrl from "@docusaurus/useBaseUrl";

## 5.1 什么是异常

异常一般是指运行期（此处特指 `Exception` 类）会发生的导致程序意外中止的问题，是一种对问题的描述后的封装对象。

在过去开发中，通常异常由系统运行时出错抛出，但现在的开发过程中，我们应在程序开发中合理的抛出异常，比如更新一条不存在的实体，或查询一个不存在的数据等等。

## 5.2 处理异常方式

- 不处理，直接中断程序执行（不推荐）
- 通过 `try catch finally` 处理（不推荐）
- 全局统一处理，并记录异常信息**（推荐）**
- 异常注解方式处理，支持**本地化** **（推荐）**

## 5.3 什么是友好异常处理

### 5.3.1 非友好异常处理

在了解友好异常处理之前可以看看非友好异常处理：

- 对终端用户抛出 `500状态码` 堆栈信息
- 大量的 `try catch` 代码，污染正常业务逻辑
- 没有规范化的异常状态码和异常消息管理
- 没有异常日志收集记录
- 不支持异常消息本地化处理
- 不支持异常策略，失败后程序立即终止
- 不支持分布式事务 CAP
- 不支持异常传播
- 返回的异常格式杂乱

### 5.3.2 友好异常处理

- 对终端用户提示友好
- 对后端开发人员提供详细的异常堆栈
- 不干扰正常业务逻辑代码，如 没有 `try catch` 代码
- 支持异常状态码多方设置
- 支持异常消息本地化
- 异常信息统一配置管理
- 支持异常策略，如重试
- 支持异常日志收集记录
- 支持 CAP 分布式事务关联
- 支持内部异常外部传播
- 支持返回统一的异常格式数据

## 5.4 友好异常处理使用示例

`Fur` 框架提供了非常灵活的友好异常处理方式。

### 5.4.1 注册友好异常服务

```cs {3,15} title="Fur.Web.Entry/Startup.cs"
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Fur.Web.Entry
{
    public class Startup
    {
        // Other Codes...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApp();
            services.AddControllers()
                    .AddFriendlyException();    // 可通过 .AddFriendlyException(false) 关闭全局异常处理
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // Other Codes...
        }
    }
}
```

:::important 特别注意

`.AddFriendlyException()` 需在 `services.AddControllers()` 之后注册。同时 `services.AddApp()` 总是最先注册。

:::

### 5.4.2 两个例子

#### 简单抛个异常

```cs {2,12}
using Fur.DynamicApiController;
using Fur.FriendlyException;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        public int Get(int id)
        {
            if (id < 3)
            {
                throw Oops.Oh($"{id} 不能小于3");
            }

            return id;
        }
    }
}
```

如下图所示：

<img src={useBaseUrl("img/yhyc1.gif")} />

#### 抛出特定类型异常

```cs {2,13}
using Fur.DynamicApiController;
using Fur.FriendlyException;
using System;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        public int Get(int id)
        {
            if (id < 3)
            {
                throw Oops.Oh($"{id} 不能小于3。", typeof(InvalidOperationException));
            }

            return id;
        }
    }
}
```

如下图所示：

<img src={useBaseUrl("img/yhyc2.png")} />

## 5.5 关于 Oops.Oh

通过上面的例子可以看出，`Oops.Oh(errorMessage)` 可以结合 `throw` 抛出异常。对于熟悉`C#`的人员来说，`throw` 后面只能 `Exception` 实例。`Oops.Oh(...)` 方法返回正是 `Exception` 实例。

### 5.5.1 为什么起这个名字？

这个名字来源于一个英语句子：`Oh, Oops!`，意思是 **噢（哎），出错了！**，所以就有了 `Oops.Oh`。

### 5.5.2 `Oops.Oh` 重载方法

```cs {13,22,30,39}
using System;

namespace Fur.FriendlyException
{
    public static class Oops
    {
        /// <summary>
        /// 抛出字符串异常
        /// </summary>
        /// <param name="errorMessage">异常消息</param>
        /// <param name="args">String.Format 参数</param>
        /// <returns>异常实例</returns>
        public static Exception Oh(string errorMessage, params object[] args);

        /// <summary>
        /// 抛出字符串异常
        /// </summary>
        /// <param name="errorMessage">异常消息</param>
        /// <param name="exceptionType">具体异常类型</param>
        /// <param name="args">String.Format 参数</param>
        /// <returns>异常实例</returns>
        public static Exception Oh(string errorMessage, Type exceptionType, params object[] args);

        /// <summary>
        /// 抛出错误码异常
        /// </summary>
        /// <param name="errorCode">错误码</param>
        /// <param name="args">String.Format 参数</param>
        /// <returns>异常实例</returns>
        public static Exception Oh(object errorCode, params object[] args);

        /// <summary>
        /// 抛出错误码异常
        /// </summary>
        /// <param name="errorCode">错误码</param>
        /// <param name="exceptionType">具体异常类型</param>
        /// <param name="args">String.Format 参数</param>
        /// <returns>异常实例</returns>
        public static Exception Oh(object errorCode, Type exceptionType, params object[] args);
    }
}
```

## 5.6 最佳实践 🤗

在 `Fur` 框架中，提供了非常灵活且规范化的友好异常处理方式，通过这个方式可以方便管理异常状态码、异常信息及异常本地化。

### 5.6.1 创建异常信息类型

实现自定义异常信息类型必须遵循以下配置：

- 类型必须是公开且时 `Enum` 枚举类型
- 枚举类型必须贴有 `[ErrorCodeType]` 特性
- 枚举中每一项必须贴有 `[ErrorCodeItemMetadata]` 特性

```cs {1,5,8,11,14,17}
using Fur.FriendlyException;

namespace Fur.Application
{
    [ErrorCodeType]
    public enum ErrorCodes
    {
        [ErrorCodeItemMetadata("{0} 不能小于 {1}")]
        z1000,

        [ErrorCodeItemMetadata("数据不存在")]
        x1000,

        [ErrorCodeItemMetadata("{0} 发现 {1} 个异常", "百小僧", 2)]
        x1001,

        [ErrorCodeItemMetadata("服务器运行异常", ErrorCode = "Error")]
        SERVER_ERROR
    }
}
```

:::important

`Fur` 框架提供了 `[ErrorCodeType]` 特性和 `IErrorCodeTypeProvider` 提供器接口来提供异常信息扫描，这里用的是 `[ErrorCodeType]` 特性类。

:::

### 5.6.2 关于 `[ErrorCodeItemMetadata]`

`Fur` 框架提供了`[ErrorCodeItemMetadata]` 特性用来标识**枚举字段**异常元数据，该特性支持传入 `消息内容` 和 `格式化参数`。最终会使用 `String.Format(消息内容，格式化参数)` 进行格式化。

如果消息内容中包含`格式化占位符`但未指定`格式化参数`，那么会查找异常所在方法是否贴有 `[IfException]` 特性且含有格式化参数，接着就会查找 `Oops.Oh` 中指定的 `格式化参数`。

### 5.6.3 静态异常类使用

```cs {2,12}
using Fur.DynamicApiController;
using Fur.FriendlyException;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        public int Get(int id)
        {
            if (id < 3)
            {
                throw Oops.Oh(ErrorCodes.z1000, id, 3);
            }

            return id;
        }
    }
}
```

如下图所示：

<img src={useBaseUrl("img/yhyc3.gif")} />

### 5.6.4 更多例子

```cs
throw Oops.Oh(1000);
throw Oops.Oh(ErrorCodes.x1000);
throw Oops.Oh("哈哈哈哈");
throw Oops.Oh(errorCode: "x1001");
throw Oops.Oh(1000, typeof(Exception));
```

## 5.7 多个异常信息类型

```cs {5-6,21-22}
using Fur.FriendlyException;

namespace Fur.Application
{
    [ErrorCodeType]
    public enum ErrorCodes
    {
        [ErrorCodeItemMetadata("{0} 不能小于 {1}")]
        z1000,

        [ErrorCodeItemMetadata("数据不存在")]
        x1000,

        [ErrorCodeItemMetadata("{0} 发现 {1} 个异常", "百小僧", 2)]
        x1001,

        [ErrorCodeItemMetadata("服务器运行异常", ErrorCode = "Error")]
        SERVER_ERROR
    }

    [ErrorCodeType]
    public enum UserErrorCodes
    {
        [ErrorCodeItemMetadata("用户数据不存在")]
        u1000,

        [ErrorCodeItemMetadata("其他异常")]
        u1001
    }
}
```

:::important 特别注意

多个异常静态类中也必须保证常量值唯一性，不可重复。

:::

## 5.8 `IErrorCodeTypeProvider` 提供器

在 `Fur` 框架中，还提供了 `IErrorCodeTypeProvider` 异常消息提供器接口，方便在不能贴 `[ErrorCodeType]` 特性情况下使用：

```cs {2,6,8-11}
using Fur.FriendlyException;
using System;

namespace Fur.Application
{
    public class CustomErrorCodeTypeProvider : IErrorCodeTypeProvider
    {
        public Type[] Definitions => new[] {
            typeof(ErrorCodes),
            typeof(ErrorCodes2)
        };
    }
}
```

使用 `IErrorCodeTypeProvider` 方式需要在 `Startup.cs` 中注册：

```cs {3,15} title="Fur.Web.Entry/Startup.cs"
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Fur.Web.Entry
{
    public class Startup
    {
        // Other Codes...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApp();
            services.AddControllers()
                    .AddFriendlyException<CustomErrorCodeTypeProvider>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // Other Codes...
        }
    }
}
```

:::tip 小知识

只有使用 `IErrorCodeTypeProvider` 方式才需使用泛型方式注册。通过上面的方式注册可以同时支持 `IErrorCodeTypeProvider` 和 `[ErrorCodeType]` 方式。

:::

## 5.9 `appsetting.json` 中配置

`Fur` 框架还提供了非常灵活的配置文件配置异常，通过这种方式可以实现异常信息后期配置，也就是无需在开发阶段预先定义。

```json {2-9} title="Fur.Web.Entry/appsettings.json"
{
  "AppSettings": {
    "ErrorCodeMessageSettings": {
      "Definitions": [
        [5000, "{0} 不能小于 {1}"],
        [5001, "我叫 {0} 名字", "百小僧"],
        [5002, "Oops! 出错了"]
      ]
    }
  }
}
```

`Definitions` 类型为二维数组，二维数组中的每一个数组第一个参数为 `ErrorCode` 也就是错误码，第二个参数为 `ErrorMessage` 消息内容，剩余参数作为 `ErrorMessage` 的格式化参数。

#### 使用示例

```cs {2,12}
using Fur.DynamicApiController;
using Fur.FriendlyException;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        public int Get(int id)
        {
            if (id < 3)
            {
                throw Oops.Oh(5000, id, 3); // 可以将 5000作为常量配置起来
            }

            return id;
        }
    }
}
```

:::tip 小知识

`[ErrorCodeType]` 和 `IErrorCodeTypeProvider` 和 `appsettings.json` 可以同时使用。

:::

## 5.10 `[IfException]` 使用

`Fur` 框架提供了 `[IfException]` 特性可以**覆盖默认消息配置**。也就是覆盖 `异常消息类型` 和 `appsettings.json` 中的配置。

:::caution 特别注意

`[IfException]` 只能贴在方法上，支持多个，而且该方法所在的类类型必须是 `ControllerBase` 子类 或 实现 `IDynamicApiController` 接口。

:::

### 5.10.1 使用示例

- 异常消息类定义

```cs {1,4}
[ErrorCodeType]
public static class ErrorCodes
{
   [ErrorCodeItemMetadata("{0} 不能小于 {1}")]
   z1000
}
```

- 覆盖默认配置

```cs {8}
using Fur.DynamicApiController;
using Fur.FriendlyException;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        [IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的：{0} 不能小于 {1}")]
        public int Get(int id)
        {
            if (id < 3)
            {
                throw Oops.Oh(ErrorCodes.z1000, id, 3);
            }

            return id;
        }
    }
}
```

如下图所示：

<img src={useBaseUrl("img/yhyc4.png")} />

### 5.10.2 更多例子

```cs {2,8-11}
using Fur.DynamicApiController;
using Fur.FriendlyException;

namespace Fur.Application
{
    public class FurAppService : IDynamicApiController
    {
        [IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的：{0} 不能小于 {1}")]
        [IfException(ErrorCodes.x1001, "格式化参数1", "格式化参数2", ErrorMessage = "我覆盖了默认的：{0} 不能小于 {1}")]
        [IfException(ErrorCodes.x1000, "格式化参数1", "格式化参数2")]
        [IfException(ErrorCodes.SERVER_ERROR, "格式化参数1", "格式化参数2")]
        public int Get(int id)
        {
            if (id < 3)
            {
                throw Oops.Oh(ErrorCodes.z1000, id, 3);
            }

            return id;
        }
    }
}
```

:::important 格式化流程

如果消息内容中包含`格式化占位符`但未指定`格式化参数`，那么会查找异常所在方法是否贴有 `[IfException]` 特性且含有格式化参数，接着就会查找 `Oops.Oh` 中指定的 `格式化参数`。

:::

## 5.11 异常消息优先级

`[ErrorCodeItemMetadata]` -> `appsettings.json` -> `[IfException]`。**（低 -> 高）**

- `[IfException]` 会覆盖 `appsettings.json` 定义的状态码消息。
- `appsettings.json` 会覆盖 `[ErrorCodeItemMetadata]` 定义的消息。

## 5.12 多语言支持

文档整理中...

## 5.13 异常模型提供器

文档整理中...

## 5.14 `Fur` 内置异常

- `EFCoreErrorCodes`：`EF Core` 操作异常
  - `DataNotFound`：未找到数据
  - `KeyNotSet`：没有设置主键

## 5.15 反馈与建议

:::note 与我们交流

给 Fur 提 [Issue](https://gitee.com/monksoul/Fur/issues/new?issue)。

:::
